﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;

namespace Bookvar.ObjectModel.Collections
{
    [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
    [Serializable]
    public abstract class ObservableKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>, INotifyPropertyChanged,
                                                                   INotifyCollectionChanged
    {
        private const string CountPropertyName = "Count";
        private const string IndexerPropertyName = "Item[]";

        private readonly SimpleMonitor monitor;

        [field: NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;
        [field: NonSerialized]
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        protected ObservableKeyedCollection()
        {
            monitor = new SimpleMonitor();
        }

        protected ObservableKeyedCollection( IEnumerable<TItem> items ) : this()
        {
            if ( items == null )
            {
                throw new ArgumentNullException( "items" );
            }
            foreach ( TItem item in items )
            {
                Add( item );
            }
        }

        protected IDisposable BlockReentrancy()
        {
            monitor.Enter();
            return monitor;
        }

        protected void CheckReentrancy()
        {
            if ( ( monitor.Busy && ( CollectionChanged != null ) ) &&
                 ( CollectionChanged.GetInvocationList().Length > 1 ) )
            {
                throw new InvalidOperationException( "ObservableKeyedCollection reentrancy not allowed" );
            }
        }

        protected override void ClearItems()
        {
            CheckReentrancy();
            base.ClearItems();
            OnPropertyChanged( CountPropertyName );
            OnPropertyChanged( IndexerPropertyName );
            OnCollectionReset();
        }

        protected override void InsertItem( int index, TItem item )
        {
            CheckReentrancy();
            base.InsertItem( index, item );
            OnPropertyChanged( CountPropertyName );
            OnPropertyChanged( IndexerPropertyName );
            OnCollectionChanged( NotifyCollectionChangedAction.Add, item, index );
        }

        public void Move( int oldIndex, int newIndex )
        {
            MoveItem( oldIndex, newIndex );
        }

        protected virtual void MoveItem( int oldIndex, int newIndex )
        {
            CheckReentrancy();
            TItem item = base[ oldIndex ];
            base.RemoveItem( oldIndex );
            base.InsertItem( newIndex, item );
            OnPropertyChanged( IndexerPropertyName );
            OnCollectionChanged( NotifyCollectionChangedAction.Move, item, newIndex, oldIndex );
        }

        protected virtual void OnCollectionChanged( NotifyCollectionChangedEventArgs e )
        {
            if ( CollectionChanged != null )
            {
                using ( BlockReentrancy() )
                {
                    CollectionChanged( this, e );
                }
            }
        }

        private void OnCollectionChanged( NotifyCollectionChangedAction action, object item, int index )
        {
            OnCollectionChanged( new NotifyCollectionChangedEventArgs( action, item, index ) );
        }

        private void OnCollectionChanged( NotifyCollectionChangedAction action, object item, int index, int oldIndex )
        {
            OnCollectionChanged( new NotifyCollectionChangedEventArgs( action, item, index, oldIndex ) );
        }

        private void OnCollectionChanged( NotifyCollectionChangedAction action, object oldItem, object newItem,
                                          int index )
        {
            OnCollectionChanged( new NotifyCollectionChangedEventArgs( action, newItem, oldItem, index ) );
        }

        private void OnCollectionReset()
        {
            OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
        }

        protected virtual void OnPropertyChanged( PropertyChangedEventArgs e )
        {
            if ( PropertyChanged != null )
            {
                PropertyChanged( this, e );
            }
        }

        private void OnPropertyChanged( string propertyName )
        {
            OnPropertyChanged( new PropertyChangedEventArgs( propertyName ) );
        }

        protected override void RemoveItem( int index )
        {
            CheckReentrancy();
            TItem item = base[ index ];
            base.RemoveItem( index );
            OnPropertyChanged( CountPropertyName );
            OnPropertyChanged( IndexerPropertyName );
            OnCollectionChanged( NotifyCollectionChangedAction.Remove, item, index );
        }

        protected override void SetItem( int index, TItem item )
        {
            CheckReentrancy();
            TItem oldItem = base[ index ];
            base.SetItem( index, item );
            OnPropertyChanged( IndexerPropertyName );
            OnCollectionChanged( NotifyCollectionChangedAction.Replace, oldItem, item, index );
        }

        public bool TryGetItem( TKey key, out TItem item )
        {
            if ( Contains( key ) )
            {
                item = this[ key ];
                return true;
            }
            item = default( TItem );
            return false;
        }

        public void AddRange(IEnumerable<TItem> items)
        {
            if (items == null)
            {
                throw new ArgumentNullException("items");
            }

            foreach (TItem item in items)
            {
                Add(item);
            }
        }

        [Serializable]
        private class SimpleMonitor : IDisposable
        {
            private int busyCount;

            [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly")]
            public void Dispose()
            {
                busyCount--;
            }

            public void Enter()
            {
                busyCount++;
            }

            public bool Busy
            {
                get { return ( busyCount > 0 ); }
            }
        }
    }
}